use std::ops::{Add, Sub};

use crate::msg::{Action, ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::pool::{read_pool_by_pool_id, read_pools, store_pool, PoolInfo};
use crate::staker::{read_staker_by_address, store_staker, Staker};
use crate::state::{read_config, store_config, ConfigInfo, REWARD_TOKEN_DECIMAL};
use cosmwasm_std::{
    log, to_binary, Api, Binary, CosmosMsg, Env, Extern, HandleResponse, HandleResult, HumanAddr,
    InitResponse, Querier, StdError, StdResult, Storage, Uint128,
};
use hermit_toolkit_hmip21::asset::AssetInfo;
use hermit_toolkit_hmip21::transfer_msg;
use hermit_toolkit_hmip721::transfer_nft_msg;

const BOLCK_SIZE: usize = 256;

pub fn init<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: InstantiateMsg,
) -> StdResult<InitResponse> {
    let owner = msg.owner.unwrap_or(env.message.sender);
    let config = ConfigInfo {
        owner,
        reward_per_block: msg.reward_per_block,
        all_points: msg.all_points,
    };

    store_config(&mut deps.storage, &config)?;

    Ok(InitResponse {
        messages: vec![],
        log: vec![log("init_nftfarm", "success")], // See https://github.com/CosmWasm/wasmd/pull/386
    })
}

pub fn handle<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: ExecuteMsg,
) -> HandleResult {
    match msg {
        ExecuteMsg::UpdateConfig {
            new_owner,
            reward_per_block,
        } => execute_update_config(deps, env, new_owner, reward_per_block),
        ExecuteMsg::AddPool { pool } => execute_add_pool(deps, env, pool),
        ExecuteMsg::Deposit {
            pool_id,
            nfttoken_id,
        } => try_deposit(deps, env, pool_id, nfttoken_id),
        ExecuteMsg::Unstake {
            pool_id,
            nfttoken_id,
        } => try_unstake(deps, env, pool_id, nfttoken_id),
        ExecuteMsg::ClaimReward { pool_id } => try_claim_reward(deps, env, pool_id),
        ExecuteMsg::WithdrawReward { pool_id } => try_withdraw_reward(deps, env, pool_id),
        ExecuteMsg::WithdrawStake {
            pool_id,
            nfttoken_id,
        } => try_withdraw_stake(deps, env, pool_id, nfttoken_id),
    }
}

pub fn try_deposit<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
    nfttoken_id: String,
) -> HandleResult {
    let current_block_time = env.block.time;
    // load config info
    let config = read_config(&deps.storage)?;
    // load pool info
    let mut pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    // load staker info
    let mut staker = Staker {
        address: env.message.sender.clone(),
        pool_id: pool_id,
        stake_amount: Uint128::from(1u64),
        reward: Uint128::zero(),
        last_block: current_block_time,
        reward_dept: Uint128::zero(),
        stake_nfttoken_ids: vec![nfttoken_id.clone()],
    };
    let staker_m = read_staker_by_address(&deps.storage, pool_id, env.message.sender.clone());
    if staker_m.is_ok() {
        staker = staker_m.unwrap();
        // update reward data
        update_reward(
            current_block_time,
            &config,
            &mut pool,
            &mut staker,
            nfttoken_id.clone(),
            Action::Deposit("Deposit".to_string()),
        );
    }
    if pool.reward_amount < staker.reward {
        return Err(StdError::generic_err("not enough reward"));
    }

    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // Generate pending reward token transfer messages
    if staker.reward > Uint128::zero() {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &pool.reward_asset
        {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                staker.reward,
                None,
                None,
                BOLCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
    }
    // Generate stake nft transfer messages
    if let AssetInfo::Token {
        contract_addr,
        token_code_hash,
        ..
    } = &pool.stake_asset
    {
        transfer_msgs.push(transfer_nft_msg(
            env.contract.address.clone(),
            nfttoken_id.clone(),
            None,
            None,
            BOLCK_SIZE,
            token_code_hash.into(),
            contract_addr.into(),
        )?);
    };
    // update reward_amount and stake_amount
    pool.reward_amount = pool.reward_amount.sub(staker.reward)?;
    pool.stake_amount = pool.stake_amount.add(Uint128::from(1u64));
    staker.reward = Uint128::zero();
    staker.last_block = current_block_time;

    // update pool info
    store_pool(&mut deps.storage, &pool)?;
    // Update stakers info
    store_staker(&mut deps.storage, &staker)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "deposit"),
            log("pool_id", pool.pool_id),
            log("reward_amount", staker.reward),
            log("nfttoken_id", nfttoken_id),
        ],
        data: None,
    })
}

pub fn try_claim_reward<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
) -> HandleResult {
    let current_block_time = env.block.time;
    // load config info
    let config = read_config(&deps.storage)?;
    // load pool info
    let mut pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    //  load staker info
    let mut staker = Staker {
        address: env.message.sender.clone(),
        pool_id: pool_id,
        stake_amount: Uint128::zero(),
        reward: Uint128::zero(),
        last_block: current_block_time,
        reward_dept: Uint128::zero(),
        stake_nfttoken_ids: vec![],
    };
    let staker_m = read_staker_by_address(&deps.storage, pool_id, env.message.sender.clone());
    if staker_m.is_ok() {
        staker = staker_m.unwrap();
        if staker.pool_id > 0u8 && staker.stake_amount > Uint128::zero() {
            // update reward data
            update_reward(
                current_block_time,
                &config,
                &mut pool,
                &mut staker,
                "".to_string(),
                Action::Deposit("ClaimReward".to_string()),
            );
        }
    }

    if staker.reward <= Uint128::zero() {
        return Err(StdError::generic_err("no reward"));
    }
    if pool.reward_amount < staker.reward {
        return Err(StdError::generic_err("not enough reward"));
    }

    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // Generate pending reward token transfer messages
    if staker.reward > Uint128::zero() {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &pool.reward_asset
        {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                staker.reward,
                None,
                None,
                BOLCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
    }
    // update reward_amount
    pool.reward_amount = pool.reward_amount.sub(staker.reward)?;
    staker.reward = Uint128::zero();
    staker.last_block = current_block_time;

    // update pool info
    store_pool(&mut deps.storage, &pool)?;
    // Update stakers info
    store_staker(&mut deps.storage, &staker)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "claim_reward"),
            log("pool_id", pool.pool_id),
            log("reward_amount", staker.reward),
        ],
        data: None,
    })
}

pub fn try_unstake<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
    nfttoken_id: String,
) -> HandleResult {
    let current_block_time = env.block.time;
    // load config info
    let config = read_config(&deps.storage)?;
    // load pool info
    let mut pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    //  load staker info
    let mut staker = Staker {
        address: env.message.sender.clone(),
        pool_id: pool_id,
        stake_amount: Uint128::zero(),
        reward: Uint128::zero(),
        last_block: current_block_time,
        reward_dept: Uint128::zero(),
        stake_nfttoken_ids: vec![],
    };
    let staker_m = read_staker_by_address(&deps.storage, pool_id, env.message.sender.clone());
    if staker_m.is_ok() {
        staker = staker_m.unwrap();
        if staker.pool_id > 0u8 && staker.stake_amount > Uint128::zero() {
            // update reward data
            update_reward(
                current_block_time,
                &config,
                &mut pool,
                &mut staker,
                nfttoken_id.clone(),
                Action::WithDraw("WithDraw".to_string()),
            );
        }
    } else {
        return Err(StdError::generic_err("nfttoken_id no stake"));
    }
    if staker.reward <= Uint128::zero() {
        return Err(StdError::generic_err("no reward"));
    }
    if pool.reward_amount < staker.reward {
        return Err(StdError::generic_err("not enough reward"));
    }

    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // Generate pending reward token transfer messages
    if staker.reward > Uint128::zero() {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &pool.reward_asset
        {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                staker.reward,
                None,
                None,
                BOLCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
    }
    // Generate pending stake nft transfer messages
    if let AssetInfo::Token {
        contract_addr,
        token_code_hash,
        ..
    } = &pool.stake_asset
    {
        transfer_msgs.push(transfer_nft_msg(
            env.message.sender.clone(),
            nfttoken_id.clone(),
            None,
            None,
            BOLCK_SIZE,
            token_code_hash.into(),
            contract_addr.into(),
        )?);
    }
    // update reward_amount and stake_amount
    pool.reward_amount = pool.reward_amount.sub(staker.reward)?;
    pool.stake_amount = pool.stake_amount.sub(Uint128::from(1u64))?;
    staker.reward = Uint128::zero();
    staker.last_block = current_block_time;

    // update pool info
    store_pool(&mut deps.storage, &pool)?;
    // Update stakers info
    store_staker(&mut deps.storage, &staker)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "withdraw"),
            log("pool_id", pool.pool_id),
            log("nfttoken_id", nfttoken_id),
            log("reward_amount", staker.reward),
        ],
        data: None,
    })
}

pub fn update_reward(
    current_block_time: u64,
    config: &ConfigInfo,
    pool: &mut PoolInfo,
    staker: &mut Staker,
    nfttoken_id: String,
    action_type: Action,
) {
    if staker.last_block == 0u64 {
        staker.last_block = current_block_time;
    }
    let cross_block = current_block_time - staker.last_block;
    if staker.stake_amount > Uint128::zero() {
        pool.acc_per_share += Uint128::from(cross_block)
            .multiply_ratio(config.reward_per_block, 1u64)
            .multiply_ratio(REWARD_TOKEN_DECIMAL, pool.stake_amount)
            .multiply_ratio(pool.point, config.all_points);
    }

    if config.reward_per_block > Uint128::zero()
        && staker.stake_amount > Uint128::zero()
        && cross_block > 0
    {
        let acc_pending_amount = pool
            .acc_per_share
            .multiply_ratio(staker.stake_amount, REWARD_TOKEN_DECIMAL)
            .sub(staker.reward_dept)
            .unwrap();
        staker.reward = acc_pending_amount;
    }

    match action_type {
        Action::Deposit(d) => {
            if d == "Deposit" {
                staker.stake_amount += Uint128::from(1u64);
                staker.stake_nfttoken_ids.push(nfttoken_id);
            }
        }
        Action::WithDraw(w) => {
            if w == "WithDraw" {
                staker.stake_amount = staker.stake_amount.sub(Uint128::from(1u64)).unwrap();
                staker.stake_nfttoken_ids.retain(
                    |item| {
                        if item.eq(&nfttoken_id) {
                            false
                        } else {
                            true
                        }
                    },
                );
            }
        }
        Action::ClaimReward(c) => {
            if staker.reward == Uint128::zero() {
                print!("{} no any reward", c);
            }
        }
    }

    staker.reward_dept = staker
        .stake_amount
        .multiply_ratio(pool.acc_per_share, REWARD_TOKEN_DECIMAL);
}

pub fn try_withdraw_reward<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
) -> StdResult<HandleResponse> {
    let config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }
    // load pool info
    let mut pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // create transfer msg
    if let AssetInfo::Token {
        contract_addr,
        token_code_hash,
        ..
    } = &pool.reward_asset
    {
        transfer_msgs.push(transfer_msg(
            env.message.sender.clone(),
            pool.reward_amount,
            None,
            None,
            BOLCK_SIZE,
            token_code_hash.clone(),
            contract_addr.clone(),
        )?);
    }

    pool.reward_amount = Uint128::zero();
    store_pool(&mut deps.storage, &pool)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "withdraw_reward"),
            log("pool_id", pool.pool_id),
            log("reward_amount", pool.reward_amount),
        ],
        data: None,
    })
}

pub fn try_withdraw_stake<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
    nfttoken_id: String,
) -> StdResult<HandleResponse> {
    let config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }
    // load pool info
    let mut pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // Generate pending stake nft transfer messages
    if let AssetInfo::Token {
        contract_addr,
        token_code_hash,
        ..
    } = &pool.stake_asset
    {
        transfer_msgs.push(transfer_nft_msg(
            env.message.sender.clone(),
            nfttoken_id.clone(),
            None,
            None,
            BOLCK_SIZE,
            token_code_hash.into(),
            contract_addr.into(),
        )?);
    }

    pool.stake_amount = pool.stake_amount.sub(Uint128::from(1u64)).unwrap();
    store_pool(&mut deps.storage, &pool)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "withdraw_stake"),
            log("pool_id", pool.pool_id),
            log("nfttoken_id", nfttoken_id),
            log("stake_amount", pool.stake_amount),
        ],
        data: None,
    })
}

pub fn execute_update_config<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    new_owner: HumanAddr,
    reward_per_block: Uint128,
) -> HandleResult {
    let mut config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }

    config.owner = new_owner.clone();
    config.reward_per_block = reward_per_block;

    store_config(&mut deps.storage, &config)?;

    Ok(HandleResponse {
        messages: vec![],
        log: vec![
            log("action", "update_config"),
            log("newowner", new_owner),
            log("reward_per_block", reward_per_block),
        ],
        data: None,
    })
}

pub fn execute_add_pool<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool: PoolInfo,
) -> HandleResult {
    let config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }
    let pool_map = read_pool_by_pool_id(&deps.storage, pool.clone().pool_id);
    if pool_map.is_ok() {
        return Err(StdError::generic_err("pool_id is exists"));
    }

    store_pool(&mut deps.storage, &pool)?;

    Ok(HandleResponse {
        messages: vec![],
        log: vec![log("action", "add_pool")],
        data: None,
    })
}

pub fn query<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    msg: QueryMsg,
) -> StdResult<Binary> {
    match msg {
        QueryMsg::Config {} => to_binary(&query_config(deps)?),
        QueryMsg::Pool { pool_id } => to_binary(&query_pool(deps, pool_id)?),
        QueryMsg::Pools {} => to_binary(&query_pools(deps)?),
        QueryMsg::Staker {
            address,
            block_time,
            pool_id,
        } => to_binary(&query_staker(deps, address, block_time, pool_id)?),
    }
}

pub fn query_config<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
) -> StdResult<ConfigInfo> {
    Ok(read_config(&deps.storage)?)
}

fn query_pool<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    pool_id: u8,
) -> StdResult<PoolInfo> {
    Ok(read_pool_by_pool_id(&deps.storage, pool_id)?)
}

fn query_pools<S: Storage, A: Api, Q: Querier>(deps: &Extern<S, A, Q>) -> StdResult<Vec<PoolInfo>> {
    Ok(read_pools(&deps)?)
}

fn query_staker<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    address: HumanAddr,
    block_time: u64,
    pool_id: u8,
) -> StdResult<Staker> {
    let current_block_time = block_time;
    // load config info
    let config = read_config(&deps.storage)?;
    // load pool info
    let mut pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    //  load staker info
    let mut staker = Staker {
        address: address.clone(),
        pool_id: pool_id,
        stake_amount: Uint128::zero(),
        reward: Uint128::zero(),
        last_block: current_block_time,
        reward_dept: Uint128::zero(),
        stake_nfttoken_ids: vec![],
    };
    let staker_m = read_staker_by_address(&deps.storage, pool_id, address.clone());
    if staker_m.is_ok() {
        staker = staker_m.unwrap();
    }
    if staker.last_block == 0u64 {
        staker.last_block = current_block_time;
    }
    let cross_block = current_block_time - staker.last_block;

    if staker.stake_amount > Uint128::zero() {
        pool.acc_per_share += Uint128::from(cross_block)
            .multiply_ratio(config.reward_per_block, 1u64)
            .multiply_ratio(REWARD_TOKEN_DECIMAL, pool.stake_amount)
            .multiply_ratio(pool.point, config.all_points);
    }

    if config.reward_per_block > Uint128::zero()
        && staker.stake_amount > Uint128::zero()
        && cross_block > 0
    {
        let acc_pending_amount = pool
            .acc_per_share
            .multiply_ratio(staker.stake_amount, REWARD_TOKEN_DECIMAL)
            .sub(staker.reward_dept)
            .unwrap();
        staker.reward = acc_pending_amount;
    }

    staker.reward_dept = staker
        .stake_amount
        .multiply_ratio(pool.acc_per_share, REWARD_TOKEN_DECIMAL);

    Ok(staker)
}
